import { Service, deduplicate, h } from "koishi"
import { readFile, writeFile, mkdir } from "fs/promises"
import { dirname, resolve as pathResolve } from "path"

const songlistBriefPath = "data/song/songlist-brief.json"

const maxNotForwardNum = 10

export default class SongService extends Service {
    static name = "song"

    songlistBrief = null

    song2str(song, only = "") {
        var singers = [song.singers[0]]
        for (let i = 1; i < song.singers.length; i++) {
            singers.push(h.i18n("song.separator"), song.singers[i])
        }
        if (only == "singer")
            return h("", singers)
        else if (only == "title")
            return song.name
        else
            return h.i18n("song.song", [h("", singers), song.name])
    }

    record2shortInfo(record, only = "") {
        if (record.placeholding.tier == "strong")
            return this.song2str(record.placeholding.song, only)
        else
            return this.song2str(record.song, only)
    }

    record2longInfo(record) {
        var ret = [this.song2str(record.song)]
        if (!!record.placeholding && record.placeholding.tier != "none") {
            var languages = record.placeholding.language.split(",")
            var arr = [h("i18n:langname", { path: "song.language", code: languages[0] })]
            for (let i = 1; i < languages.length; i++) {
                arr.push(h.i18n("song.separator"), h("i18n:langname", { path: "song.language", code: languages[i] }))
            }
            ret.push(h.i18n("general.comma"), h.i18n("song.placeholding", [h("", arr), this.song2str(record.placeholding.song)]))
        }
        if (record.alternatives.length > 0) {
            var alts = [this.song2str(record.alternatives[0])]
            for (let i = 1; i < record.alternatives.length; i++) {
                alts.push(h.i18n("song.separator"), this.song2str(record.alternatives[i]))
            }
            ret.push(h.i18n("general.comma"), h.i18n("song.alternatives", [h("", alts)]))
        }
        return ret
    }

    songInfo(numstr, detailed = false, only = "") {
        //if (!(/^\d+$/.test(numstr)))
        //    return h.i18n("song.not-integer")
        var num = Number(numstr)
        if (only == "song" || only == "singer" || only == "title")
            return this.record2shortInfo(this.songlistBrief.songlist[num], only)
        if (!!this.songlistBrief.songlist[num]) {
            if (detailed)
                return h.i18n("song.song-info", [num, h("", this.record2longInfo(this.songlistBrief.songlist[num]))])
            else
                return h.i18n("song.song-info", [num, h("", this.record2shortInfo(this.songlistBrief.songlist[num]))])
        }
        else {
            return h.i18n("song.not-exist", [num])
        }
    }

    withNumUnsafe(numAndRecord) {
        return h.i18n("song.with-num", [numAndRecord[0], h("", this.record2longInfo(numAndRecord[1]))])
    }

    withNumListUnsafe(records, maxn = 10) {
        var ret = [h("p", this.withNumUnsafe(records[0]))]
        var endn = records.length
        if (endn > maxn && maxn > 0) endn = maxn
        for (let i = 1; i < endn; i++) {
            ret.push(h("p", this.withNumUnsafe(records[i])))
        }
        if (records.length > endn) {
            ret.push(h("p", h.i18n("song.etc")))
        }
        if (endn > maxNotForwardNum) return h("message", { forward: true }, ret)
        return ret
    }

    async updateData() {
        const json = await this.ctx.http.get("https://wiki.xdi8.top/index.php?action=raw&title=Module:Songlist-brief.json", { responseType: "text" })
        this.songlistBrief = JSON.parse(json)
        await writeFile(this.songlistBriefPath, json)
    }

    constructor(ctx) {
        super(ctx, "lnnbotSong", true)

        this.songlistBriefPath = pathResolve(ctx.baseDir, songlistBriefPath)

        readFile(this.songlistBriefPath, "utf-8").then(
            s => {
                this.songlistBrief = JSON.parse(s)
            },
            async () => {
                await mkdir(dirname(this.songlistBriefPath), { recursive: true })
                this.updateData().catch(() => {})
            },
        )

        ctx.command("song <numbers:rawtext>", { checkUnknown: true })
        .option("detailed", "-d")
        .option("version", "-v")
        .option("update", "-u", { authority: 2 })
        .option("only", "-o <song|singer|title>")
        .action(({ root, session, options }, str) => {
            if (options.version) return session.text(".version", [this.songlistBrief.version])
            if (options.update) return this.updateData().then(() => h.i18n(".update.success", [this.songlistBrief.version]))

            const matches = str?.match(/\d+/g) ?? []
            const nums = deduplicate(matches.map(Number).filter(n => n <= 0x7fff_ffff))
            if (!nums.length) return session.text(".invalid")

            if (!!options.only)
                return this.songInfo(nums[0], options.detailed, options.only)

            var out = [this.songInfo(nums[0], options.detailed)]
            for (let i = 1; i < nums.length; i++) {
                out.push(h("br"))
                out.push(this.songInfo(nums[i], options.detailed))
            }
            if (nums.length > maxNotForwardNum && root) return h("message", { forward: true }, out)
            return out
        })
        ctx.i18n.define("zh-CN", "commands.song", {
            description: "检索荆哲歌单歌曲信息",
            arguments: { "numbers": "歌曲编号" },
            options: {
                "detailed": "显示详细信息",
                "version": "显示数据库版本号",
                "update": "更新歌单数据",
                "only": "只显示歌曲关键部分信息，song为歌曲，singer为歌手，title为歌名"
            },
            messages: {
                "invalid": "输入无效…",
                "version": "当前歌单数据库版本号为 {0}。",
                "update": {
                    "success": "歌单数据同步成功，<i18n path=\"commands.song.messages.version\">{0}</i18n>"
                }
            }
        })
        ctx.i18n.define("en-US", "commands.song", {
            description: "Query for song infomation in the songlist",
            arguments: { "numbers": "Song number" },
            options: {
                "detailed": "Show the detailed infomation",
                "version": "Query for the version of database",
                "update": "Update songlist database",
                "only": "Show specific infomation only: 'song' for the song, 'singer' for singers, 'title' for the title of the song"
            },
            messages: {
                "invalid": "Invalid input...",
                "version": "Current version of database is {0}.",
                "update": {
                    "success": "Songlist database successfully synced, current version is {0}."
                }
            }
        })

        ctx.command("song-search <content:rawtext>", { checkUnknown: true })
        .option("grep", "-g")
        .option("singer", "-s")
        .option("standard", "-t")
        .option("maximum", "-m <n:natural>")
        .action(({ session, options }, info) => {
            if (info === undefined) return session.i18n("internal.insufficient-arguments")
            var sl = Object.entries(this.songlistBrief.songlist);
            var res = []
            var message = ""
            if (options.grep) {
                var re = new RegExp(info, "ui")
                if (options.singer) {
                    message = ".singer-res-grep"
                    res = sl.map(([key, value]) => {
                        if (options.standard && !value.standard)
                            return null
                        if (value.song.singers.some( v => re.test(v) )) return [key, value]
                        if (!!value.placeholding.tier && value.placeholding.tier != "none") {
                            if (value.placeholding.song.singers.some( v => re.test(v) )) return [key, value]
                        }
                        for (let v of value.alternatives)
                            if (v.singers.some( vv => re.test(vv) ))
                                return [key, value]
                        return null
                    }).filter( v => !!v )
                }
                else {
                    message = ".name-res-grep"
                    res = sl.map(([key, value]) => {
                        if (options.standard && !value.standard)
                            return null
                        if (re.test(value.song.name)) return [key, value]
                        if (!!value.placeholding.tier && value.placeholding.tier != "none") {
                            if (re.test(value.placeholding.song.name)) return [key, value]
                        }
                        for (let v of value.alternatives)
                            if (re.test(v.name))
                                return [key, value]
                        return null
                    }).filter( v => !!v )
                }
            }
            else {
                const infoLower = info.toLowerCase() // 统一成小写再进行比较
                if (options.singer) {
                    message = ".singer-res"
                    res = sl.map(([key, value]) => {
                        if (options.standard && !value.standard)
                            return null
                        if (value.song.singers.some( v => v.toLowerCase() == infoLower )) return [key, value]
                        if (!!value.placeholding.tier && value.placeholding.tier != "none") {
                            if (value.placeholding.song.singers.some( v => v.toLowerCase() == infoLower )) return [key, value]
                        }
                        for (let v of value.alternatives)
                            if (v.singers.some( vv => vv.toLowerCase() == infoLower ))
                                return [key, value]
                        return null
                    }).filter( v => !!v )
                }
                else {
                    message = ".name-res"
                    res = sl.map(([key, value]) => {
                        if (options.standard && !value.standard)
                            return null
                        if (value.song.name.toLowerCase() == infoLower) return [key, value]
                        if (!!value.placeholding.tier && value.placeholding.tier != "none") {
                            if (value.placeholding.song.name.toLowerCase() == infoLower) return [key, value]
                        }
                        for (let v of value.alternatives)
                            if (v.name.toLowerCase() == infoLower)
                                return [key, value]
                        return null
                    }).filter( v => !!v )
                }
            }
            if (res.length == 0) return session.text(".nothing")
            if (typeof options.maximum === "number")
                return session.text(message, [res.length, h("", this.withNumListUnsafe(res, options.maximum)), info])
            else
                return session.text(message, [res.length, h("", this.withNumListUnsafe(res)), info])
        })
        ctx.i18n.define("zh-CN", "commands.song-search", {
            description: "搜索荆哲歌单中的歌曲",
            arguments: { content: "搜索内容" },
            options: {
                "grep": "使用正则表达式模糊搜索",
                "singer": "用歌手来搜索（默认为歌名）",
                "standard": "只在标准范围搜索",
                "maximum": "最多显示的记录条数（默认 10 条，若设为 0 则全部显示）"
            },
            messages: {
                "nothing": "目前暂无符合条件的歌曲，期待您的投稿...",
                "singer-res": "歌手包含{2}的歌曲共有 {0} 首：\n{1}",
                "singer-res-grep": "歌手符合条件的歌曲共有 {0} 首：\n{1}",
                "name-res": "歌名为《{2}》的歌曲共有 {0} 首：\n{1}",
                "name-res-grep": "歌名符合条件的歌曲共有 {0} 首：\n{1}"
            }
        })
        ctx.i18n.define("en-US", "commands.song-search", {
            description: "Search in the songlist by keyword",
            arguments: { content: "Content" },
            options: {
                "grep": "Use regular expression",
                "singer": "Search by singer (song name is default)",
                "standard": "Standard range only",
                "maximum": "maximum records displayed (10 is default, display all if set to 0)"
            },
            messages: {
                "nothing": "There are no such song yet. Welcome for your submission...",
                "singer-res": "There are {0} songs with singer {2}:\n{1}",
                "singer-res-grep": "There are {0} songs with  matching singer:\n{1}",
                "name-res": "There are {0} songs with name {2}:\n{1}",
                "name-res-grep": "There are {0} songs with matching name:\n{1}"
            }
        })

        ctx.i18n.define("zh-CN", "song", {
            "separator": "、",
            "song": "{0}的《{1}》",
            "placeholding": "原版为{0}的{1}",
            "alternatives": "另有{0}等版本",
            "etc": "等等...",
            "song-info": "{0} 号歌曲是{1}。",
            "with-num": "{0} 号{1}",
            "not-exist": "{0} 号歌曲目前暂未填充，期待您的投稿...",
            language: {
                cmn: "普通话",
                zxx: "纯音乐",
                qsd: "希顶语",
                art: "某人造语言",
            },
            //"not-integer": "请输入自然数..."
        })
        ctx.i18n.define("en-US", "song", {
            "separator": ", ",
            "song": "{0}\x27s {1}",
            "placeholding": "original version ({0}): {1}",
            "alternatives": "alternative version(s): {0}",
            "etc": "and more...",
            "song-info": "Song #{0} is {1}.",
            "with-num": "#{0}: {1}",
            "not-exist": "Song #{0} is not filled yet. We welcome your submission...",
            language: {
                cmn: "Mandarin Chinese",
                zxx: "Instrumental",
                qsd: "Shidinn",
                art: "Artificial language",
            },
            //"not-integer": "Natural numbers only..."
        })

    }
}
